/**
 * make some change to the data and rerender the virtual dom
 * then diff the previous vnode tree and the current vnode tree
 * finally patch those diffs to the real dom
 */
;(function() {
  // 定义虚拟dom
  function vnode(tag, data, children, text, elm) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
  }
  // 子元素如果是字符串，则创建 textNode，否则返回子元素，给调用函数继续遍历
  function normalizeChildren(children) {
    if (typeof children === 'string') {
      return [createTextVNode(children)]
    }
    return children
  }

  function createTextVNode(val) {
    return new vnode(undefined, undefined, undefined, String(val))
  }

  // 创建虚拟dom
  function createElement(tag, data, children) {
    return new vnode(tag, data, normalizeChildren(children), undefined, undefined)
  }

  // 创建真实dom
  function createElm(vnode) {
    var tag = vnode.tag
    var data = vnode.data
    var children = vnode.children

    if (tag !== undefined) {
      vnode.elm = document.createElement(tag)

      if (data.attrs !== undefined) {
        var attrs = data.attrs
        Object.keys(attrs).forEach(function(key) {
          vnode.elm.setAttribute(key, attrs[key])
        })
      }

      if (children) {
        createChildren(vnode, children)
      }
    } else {
      vnode.elm = document.createTextNode(vnode.text)
    }

    return vnode.elm
  }

  // 递归创建嵌套节点
  function createChildren(vnode, children) {
    for (var i = 0; i < children.length; i++) {
      vnode.elm.appendChild(createElm(children[i]))
    }
  }

  function sameVnode(vnode1, vnode2) {
    return vnode1.tag === vnode2.tag
  }

  function emptyNodeAt(elm) {
    return new vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm)
  }

  // 对比 virtual dom
  function patchVnode(oldVnode, vnode) {
    var elm = (oldVnode.elm = vnode.elm)
    var oldChildren = oldVnode.children
    var children = vnode.children
    if (!vnode.text) {
      if (oldChildren && children) {
        updateChildren(oldChildren, children)
      }
    } else {
      elm.textContent = vnode.text
    }
  }

  function updateChildren(oldChildren, children) {
    // 为了简化diff算法，假设每个元素节点只有一个子节点
    if (sameVnode(oldChildren[0], children[0])) {
      patchVnode(oldChildren[0], children[0])
    } else {
      // 对比出不同节点的地方，更新真实 dom
      patch(oldChildren[0], children[0])
    }
  }

  // virtual dom -> real dom
  function patch(oldVnode, vnode) {
    var isRealElement = oldVnode.nodeType !== undefined
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode)
    } else {
      if (isRealElement) {
        oldVnode = emptyNodeAt(oldVnode)
      }

      var elm = oldVnode.elm
      var parent = elm.parentNode

      createElm(vnode)

      parent.insertBefore(vnode.elm, elm)
      parent.removeChild(elm)
    }

    return vnode.elm
  }

  function initData(vm) {
    var data = (vm.$data = vm.$options.data)
    Object.keys(data).forEach(function(key) {
      proxy(vm, key)
    })
  }

  // 代理，this.xx 实际访问的是 this.$data[xx]
  function proxy(vm, key) {
    Object.defineProperty(vm, key, {
      configurable: true,
      enumerable: true,
      get: function() {
        return vm.$data[key]
      },
      set: function(value) {
        vm.$data[key] = value
      },
    })
  }

  function Vue(options) {
    var vm = this
    vm.$options = options

    initData(vm)
    vm.mount(document.querySelector(options.el))
  }

  Vue.prototype.mount = function(el) {
    var vm = this
    vm.$el = el
    vm.update(vm.render())
  }
  Vue.prototype.update = function(vnode) {
    var vm = this
    var prevVnode = vnode._vnode
    vnode._vnode = vnode
    if (!prevVnode) {
      vm.$el = vm.patch(vm.$el, vnode)
    } else {
      vm.$el = vm.patch(prevVnode, vnode)
    }
  }
  Vue.prototype.patch = patch
  // 渲染函数，生成 virtual dom
  Vue.prototype.render = function(vnode) {
    var vm = this
    return vm.$options.render.call(vm)
  }

  var vm = new Vue({
    el: '#app',
    data: {
      message: 'Hello world',
      isShow: true,
    },
    render() {
      return createElement(
        'div',
        {
          attrs: {
            class: 'wrapper',
          },
        },
        [
          this.isShow
            ? createElement(
                'p',
                {
                  attrs: {
                    class: 'inner',
                  },
                },
                this.message
              )
            : createElement(
                'h1',
                {
                  attrs: {
                    class: 'inner',
                  },
                },
                'Hello world'
              ),
        ]
      )
    },
  })

  // test
  setTimeout(function() {
    vm.message = 'Hello'
    vm.update(vm.render())
  }, 1000)

  setTimeout(function() {
    vm.isShow = false
    vm.update(vm.render())
  }, 2000)

})()
